/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
import {zoneSymbol} from '../../lib/common/utils';

describe('Zone', function() {
  const rootZone = Zone.current;

  it('should have a name', function() {
    expect(Zone.current.name).toBeDefined();
  });

  describe('hooks', function() {
    it('should throw if onError is not defined', function() {
      expect(function() {
        Zone.current.run(throwError);
      }).toThrow();
    });


    it('should fire onError if a function run by a zone throws', function() {
      const errorSpy = jasmine.createSpy('error');
      const myZone = Zone.current.fork({name: 'spy', onHandleError: errorSpy});

      expect(errorSpy).not.toHaveBeenCalled();

      expect(function() {
        myZone.runGuarded(throwError);
      }).not.toThrow();

      expect(errorSpy).toHaveBeenCalled();
    });

    it('should send correct currentZone in hook method when in nested zone', function() {
      const zone = Zone.current;
      const zoneA = zone.fork({
        name: 'A',
        onInvoke: function(
            parentDelegate, currentZone, targetZone, callback, applyThis, applyArgs, source) {
          expect(currentZone.name).toEqual('A');
          return parentDelegate.invoke(targetZone, callback, applyThis, applyArgs, source);
        }
      });
      const zoneB = zoneA.fork({
        name: 'B',
        onInvoke: function(
            parentDelegate, currentZone, targetZone, callback, applyThis, applyArgs, source) {
          expect(currentZone.name).toEqual('B');
          return parentDelegate.invoke(targetZone, callback, applyThis, applyArgs, source);
        }
      });
      const zoneC = zoneB.fork({name: 'C'});
      zoneC.run(function() {});
    });
  });

  it('should allow zones to be run from within another zone', function() {
    const zone = Zone.current;
    const zoneA = zone.fork({name: 'A'});
    const zoneB = zone.fork({name: 'B'});

    zoneA.run(function() {
      zoneB.run(function() {
        expect(Zone.current).toBe(zoneB);
      });
      expect(Zone.current).toBe(zoneA);
    });
    expect(Zone.current).toBe(zone);
  });


  describe('wrap', function() {
    it('should throw if argument is not a function', function() {
      expect(function() {
        (<Function>Zone.current.wrap)(11);
      }).toThrowError('Expecting function got: 11');
    });
  });

  describe('run out side of current zone', function() {
    it('should be able to get root zone', function() {
      Zone.current.fork({name: 'testZone'}).run(function() {
        expect(Zone.root.name).toEqual('<root>');
      });
    });

    it('should be able to get run under rootZone', function() {
      Zone.current.fork({name: 'testZone'}).run(function() {
        Zone.root.run(() => {
          expect(Zone.current.name).toEqual('<root>');
        });
      });
    });

    it('should be able to get run outside of current zone', function() {
      Zone.current.fork({name: 'testZone'}).run(function() {
        Zone.root.fork({name: 'newTestZone'}).run(() => {
          expect(Zone.current.name).toEqual('newTestZone');
          expect(Zone.current.parent!.name).toEqual('<root>');
        });
      });
    });
  });

  describe('get', function() {
    it('should store properties', function() {
      const testZone = Zone.current.fork({name: 'A', properties: {key: 'value'}});
      expect(testZone.get('key')).toEqual('value');
      expect(testZone.getZoneWith('key')).toEqual(testZone);
      const childZone = testZone.fork({name: 'B', properties: {key: 'override'}});
      expect(testZone.get('key')).toEqual('value');
      expect(testZone.getZoneWith('key')).toEqual(testZone);
      expect(childZone.get('key')).toEqual('override');
      expect(childZone.getZoneWith('key')).toEqual(childZone);
    });
  });

  describe('task', () => {
    function noop() {}
    let log: any[];
    const zone: Zone = Zone.current.fork({
      name: 'parent',
      onHasTask: (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState):
          void => {
            (hasTaskState as any)['zone'] = target.name;
            log.push(hasTaskState);
          },
      onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task) => {
        // Do nothing to prevent tasks from being run on VM turn;
        // Tests run task explicitly.
        return task;
      }
    });

    beforeEach(() => {
      log = [];
    });

    it('task can only run in the zone of creation', () => {
      const task =
          zone.fork({name: 'createZone'}).scheduleMacroTask('test', noop, undefined, noop, noop);
      expect(() => {
        Zone.current.fork({name: 'anotherZone'}).runTask(task);
      })
          .toThrowError(
              'A task can only be run in the zone of creation! (Creation: createZone; Execution: anotherZone)');
      task.zone.cancelTask(task);
    });

    it('task can only cancel in the zone of creation', () => {
      const task =
          zone.fork({name: 'createZone'}).scheduleMacroTask('test', noop, undefined, noop, noop);
      expect(() => {
        Zone.current.fork({name: 'anotherZone'}).cancelTask(task);
      })
          .toThrowError(
              'A task can only be cancelled in the zone of creation! (Creation: createZone; Execution: anotherZone)');
      task.zone.cancelTask(task);
    });

    it('should prevent double cancellation', () => {
      const task =
          zone.scheduleMacroTask('test', () => log.push('macroTask'), undefined, noop, noop);
      zone.cancelTask(task);
      try {
        zone.cancelTask(task);
      } catch (e) {
        expect(e.message).toContain(
            'macroTask \'test\': can not transition to \'canceling\', expecting state \'scheduled\' or \'running\', was \'notScheduled\'.');
      }
    });

    it('should not decrement counters on periodic tasks', () => {
      zone.run(() => {
        const task = zone.scheduleMacroTask(
            'test', () => log.push('macroTask'), {isPeriodic: true}, noop, noop);
        zone.runTask(task);
        zone.runTask(task);
        zone.cancelTask(task);
      });
      expect(log).toEqual([
        {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'parent'},
        'macroTask', 'macroTask', {
          microTask: false,
          macroTask: false,
          eventTask: false,
          change: 'macroTask',
          zone: 'parent'
        }
      ]);
    });

    it('should notify of queue status change', () => {
      zone.run(() => {
        const z = Zone.current;
        z.runTask(z.scheduleMicroTask('test', () => log.push('microTask')));
        z.cancelTask(
            z.scheduleMacroTask('test', () => log.push('macroTask'), undefined, noop, noop));
        z.cancelTask(
            z.scheduleEventTask('test', () => log.push('eventTask'), undefined, noop, noop));
      });
      expect(log).toEqual([
        {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
        'microTask',
        {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
        {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'parent'},
        {microTask: false, macroTask: false, eventTask: false, change: 'macroTask', zone: 'parent'},
        {microTask: false, macroTask: false, eventTask: true, change: 'eventTask', zone: 'parent'},
        {
          microTask: false,
          macroTask: false,
          eventTask: false,
          change: 'eventTask',
          zone: 'parent'
        }
      ]);
    });

    it('should notify of queue status change on parent task', () => {
      zone.fork({name: 'child'}).run(() => {
        const z = Zone.current;
        z.runTask(z.scheduleMicroTask('test', () => log.push('microTask')));
      });
      expect(log).toEqual([
        {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'child'},
        {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
        'microTask',
        {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'child'},
        {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
      ]);
    });

    it('should allow rescheduling a task on a separate zone', () => {
      const log: any[] = [];
      const zone = Zone.current.fork({
        name: 'test-root',
        onHasTask:
            (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => {
              (hasTaskState as any)['zone'] = target.name;
              log.push(hasTaskState);
            }
      });
      const left = zone.fork({name: 'left'});
      const right = zone.fork({
        name: 'right',
        onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task => {
          log.push(
              {pos: 'before', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
          // Cancel the current scheduling of the task
          task.cancelScheduleRequest();
          // reschedule on a different zone.
          task = left.scheduleTask(task);
          log.push(
              {pos: 'after', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
          return task;
        }
      });
      const rchild = right.fork({
        name: 'rchild',
        onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task => {
          log.push(
              {pos: 'before', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
          task = delegate.scheduleTask(target, task);
          log.push(
              {pos: 'after', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
          expect((task as any)._zoneDelegates.map((zd: ZoneDelegate) => zd.zone.name)).toEqual([
            'left', 'test-root', 'ProxyZone'
          ]);
          return task;
        }
      });

      const task = rchild.scheduleMacroTask('testTask', () => log.push('WORK'), {}, noop, noop);
      expect(task.zone).toEqual(left);
      log.push(task.zone.name);
      task.invoke();
      expect(log).toEqual([
        {pos: 'before', method: 'onScheduleTask', zone: 'rchild', task: 'rchild'},
        {pos: 'before', method: 'onScheduleTask', zone: 'right', task: 'rchild'},
        {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'left'}, {
          microTask: false,
          macroTask: true,
          eventTask: false,
          change: 'macroTask',
          zone: 'test-root'
        },
        {pos: 'after', method: 'onScheduleTask', zone: 'right', task: 'left'},
        {pos: 'after', method: 'onScheduleTask', zone: 'rchild', task: 'left'}, 'left', 'WORK',
        {microTask: false, macroTask: false, eventTask: false, change: 'macroTask', zone: 'left'}, {
          microTask: false,
          macroTask: false,
          eventTask: false,
          change: 'macroTask',
          zone: 'test-root'
        }
      ]);
    });

    it('period task should not transit to scheduled state after being cancelled in running state',
       () => {
         const zone = Zone.current.fork({name: 'testZone'});

         const task = zone.scheduleMacroTask('testPeriodTask', () => {
           zone.cancelTask(task);
         }, {isPeriodic: true}, () => {}, () => {});

         task.invoke();
         expect(task.state).toBe('notScheduled');
       });

    it('event task should not transit to scheduled state after being cancelled in running state',
       () => {
         const zone = Zone.current.fork({name: 'testZone'});

         const task = zone.scheduleEventTask('testEventTask', () => {
           zone.cancelTask(task);
         }, undefined, () => {}, () => {});

         task.invoke();
         expect(task.state).toBe('notScheduled');
       });

    describe('assert ZoneAwarePromise', () => {
      it('should not throw when all is OK', () => {
        Zone.assertZonePatched();
      });

      it('should keep ZoneAwarePromise has been patched', () => {
        class WrongPromise {
          static resolve(value: any) {}

          then() {}
        }

        const ZoneAwarePromise = global.Promise;
        const NativePromise = (global as any)[zoneSymbol('Promise')];
        global.Promise = WrongPromise;
        try {
          expect(ZoneAwarePromise).toBeTruthy();
          Zone.assertZonePatched();
          expect(global.Promise).toBe(ZoneAwarePromise);
        } finally {
          // restore it.
          global.Promise = NativePromise;
        }
        Zone.assertZonePatched();
      });
    });
  });

  describe('invoking tasks', () => {
    let log: string[];
    function noop() {}


    beforeEach(() => {
      log = [];
    });

    it('should not drain the microtask queue too early', () => {
      const z = Zone.current;
      const event = z.scheduleEventTask('test', () => log.push('eventTask'), undefined, noop, noop);

      z.scheduleMicroTask('test', () => log.push('microTask'));

      const macro = z.scheduleMacroTask('test', () => {
        event.invoke();
        // At this point, we should not have invoked the microtask.
        expect(log).toEqual(['eventTask']);
      }, undefined, noop, noop);

      macro.invoke();
    });

    it('should convert task to json without cyclic error', () => {
      const z = Zone.current;
      const event = z.scheduleEventTask('test', () => {}, undefined, noop, noop);
      const micro = z.scheduleMicroTask('test', () => {});
      const macro = z.scheduleMacroTask('test', () => {}, undefined, noop, noop);
      expect(function() {
        JSON.stringify(event);
      }).not.toThrow();
      expect(function() {
        JSON.stringify(micro);
      }).not.toThrow();
      expect(function() {
        JSON.stringify(macro);
      }).not.toThrow();
    });

    it('should call onHandleError callback when zoneSpec onHasTask throw error', () => {
      const spy = jasmine.createSpy('error');
      const hasTaskZone = Zone.current.fork({
        name: 'hasTask',
        onHasTask:
            (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
             hasTasState: HasTaskState) => {
              throw new Error('onHasTask Error');
            },
        onHandleError:
            (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: Error) => {
              spy(error.message);
              return delegate.handleError(targetZone, error);
            }
      });

      const microTask = hasTaskZone.scheduleMicroTask('test', () => {}, undefined, () => {});
      expect(spy).toHaveBeenCalledWith('onHasTask Error');
    });
  });
});

function throwError() {
  throw new Error();
}
